@@ -34,10 +34,67 @@ class @Utils |
||
| 34 | 34 |
body?(modal.querySelector('.modal-body'))
|
| 35 | 35 |
$(modal).modal('show')
|
| 36 | 36 |
|
| 37 |
- @handleDryRunButton: (button, data = $(button.form).serialize()) -> |
|
| 37 |
+ @handleDryRunButton: (button, data = if button.form then $(':input[name!="_method"]', button.form).serialize() else '') ->
|
|
| 38 | 38 |
$(button).prop('disabled', true)
|
| 39 |
+ cleanup = -> $(button).prop('disabled', false)
|
|
| 40 |
+ |
|
| 41 |
+ url = $(button).data('action-url')
|
|
| 42 |
+ with_event_mode = $(button).data('with-event-mode')
|
|
| 43 |
+ |
|
| 44 |
+ if with_event_mode is 'no' |
|
| 45 |
+ return @invokeDryRun(url, data, cleanup) |
|
| 46 |
+ |
|
| 47 |
+ Utils.showDynamicModal """ |
|
| 48 |
+ <h5>Event to send#{if with_event_mode is 'maybe' then ' (Optional)' else ''}</h5>
|
|
| 49 |
+ <form class="dry-run-form" method="post"> |
|
| 50 |
+ <div class="form-group"> |
|
| 51 |
+ <textarea rows="10" name="event" class="payload-editor" data-height="200"> |
|
| 52 |
+ {}
|
|
| 53 |
+ </textarea> |
|
| 54 |
+ </div> |
|
| 55 |
+ <div class="form-group"> |
|
| 56 |
+ <input value="Dry Run" class="btn btn-primary" type="submit" /> |
|
| 57 |
+ </div> |
|
| 58 |
+ </form> |
|
| 59 |
+ """, |
|
| 60 |
+ body: (body) => |
|
| 61 |
+ form = $(body).find('.dry-run-form')
|
|
| 62 |
+ payload_editor = form.find('.payload-editor')
|
|
| 63 |
+ if previous = $(button).data('payload')
|
|
| 64 |
+ payload_editor.text(previous) |
|
| 65 |
+ window.setupJsonEditor(payload_editor) |
|
| 66 |
+ form.submit (e) => |
|
| 67 |
+ e.preventDefault() |
|
| 68 |
+ json = $(e.target).find('.payload-editor').val()
|
|
| 69 |
+ json = '{}' if json == ''
|
|
| 70 |
+ try |
|
| 71 |
+ payload = JSON.parse(json) |
|
| 72 |
+ throw true unless payload.constructor is Object |
|
| 73 |
+ if Object.keys(payload).length == 0 |
|
| 74 |
+ json = '' |
|
| 75 |
+ else |
|
| 76 |
+ json = JSON.stringify(payload) |
|
| 77 |
+ catch |
|
| 78 |
+ alert 'Invalid JSON object.' |
|
| 79 |
+ return |
|
| 80 |
+ if json == '' |
|
| 81 |
+ if with_event_mode is 'yes' |
|
| 82 |
+ alert 'Event is required for this agent to run.' |
|
| 83 |
+ return |
|
| 84 |
+ dry_run_data = data |
|
| 85 |
+ $(button).data('payload', null)
|
|
| 86 |
+ else |
|
| 87 |
+ dry_run_data = "event=#{encodeURIComponent(json)}&#{data}"
|
|
| 88 |
+ $(button).data('payload', json)
|
|
| 89 |
+ $(body).closest('[role=dialog]').on 'hidden.bs.modal', =>
|
|
| 90 |
+ @invokeDryRun(url, dry_run_data, cleanup) |
|
| 91 |
+ .modal('hide')
|
|
| 92 |
+ title: 'Dry Run' |
|
| 93 |
+ onHide: cleanup |
|
| 94 |
+ |
|
| 95 |
+ @invokeDryRun: (url, data, callback) -> |
|
| 39 | 96 |
$('body').css(cursor: 'progress')
|
| 40 |
- $.ajax type: 'POST', url: $(button).data('action-url'), dataType: 'json', data: data
|
|
| 97 |
+ $.ajax type: 'POST', url: url, dataType: 'json', data: data |
|
| 41 | 98 |
.always => |
| 42 | 99 |
$('body').css(cursor: 'auto')
|
| 43 | 100 |
.done (json) => |
@@ -55,7 +112,7 @@ class @Utils |
||
| 55 | 112 |
find('.agent-dry-run-events').text(json.events).end().
|
| 56 | 113 |
find('.agent-dry-run-memory').text(json.memory)
|
| 57 | 114 |
title: 'Dry Run Results', |
| 58 |
- onHide: -> $(button).prop('disabled', false)
|
|
| 115 |
+ onHide: callback |
|
| 59 | 116 |
.fail (xhr, status, error) -> |
| 60 | 117 |
alert('Error: ' + error)
|
| 61 |
- $(button).prop('disabled', false)
|
|
| 118 |
+ callback() |
@@ -1,7 +1,7 @@ |
||
| 1 | 1 |
module DryRunnable |
| 2 | 2 |
extend ActiveSupport::Concern |
| 3 | 3 |
|
| 4 |
- def dry_run! |
|
| 4 |
+ def dry_run!(event = nil) |
|
| 5 | 5 |
@dry_run = true |
| 6 | 6 |
|
| 7 | 7 |
log = StringIO.new |
@@ -13,7 +13,12 @@ module DryRunnable |
||
| 13 | 13 |
begin |
| 14 | 14 |
raise "#{short_type} does not support dry-run" unless can_dry_run?
|
| 15 | 15 |
readonly! |
| 16 |
- check |
|
| 16 |
+ if event |
|
| 17 |
+ raise "This agent cannot receive an event!" unless can_receive_events? |
|
| 18 |
+ receive([event]) |
|
| 19 |
+ else |
|
| 20 |
+ check |
|
| 21 |
+ end |
|
| 17 | 22 |
rescue => e |
| 18 | 23 |
error "Exception during dry-run. #{e.message}: #{e.backtrace.join("\n")}"
|
| 19 | 24 |
end |
@@ -37,7 +37,7 @@ class AgentsController < ApplicationController |
||
| 37 | 37 |
def dry_run |
| 38 | 38 |
attrs = params[:agent] || {}
|
| 39 | 39 |
if agent = current_user.agents.find_by(id: params[:id]) |
| 40 |
- # PUT /agents/:id/dry_run |
|
| 40 |
+ # POST /agents/:id/dry_run |
|
| 41 | 41 |
if attrs.present? |
| 42 | 42 |
type = agent.type |
| 43 | 43 |
agent = Agent.build_for_type(type, current_user, attrs) |
@@ -50,7 +50,13 @@ class AgentsController < ApplicationController |
||
| 50 | 50 |
agent.name ||= '(Untitled)' |
| 51 | 51 |
|
| 52 | 52 |
if agent.valid? |
| 53 |
- results = agent.dry_run! |
|
| 53 |
+ if event_payload = params[:event] |
|
| 54 |
+ dummy_agent = Agent.build_for_type('ManualEventAgent', current_user, name: 'Dry-Runner')
|
|
| 55 |
+ dummy_agent.readonly! |
|
| 56 |
+ event = dummy_agent.events.build(user: current_user, payload: event_payload) |
|
| 57 |
+ end |
|
| 58 |
+ |
|
| 59 |
+ results = agent.dry_run!(event) |
|
| 54 | 60 |
|
| 55 | 61 |
render json: {
|
| 56 | 62 |
log: results[:log], |
@@ -37,4 +37,16 @@ module AgentHelper |
||
| 37 | 37 |
}.join(delimiter).html_safe |
| 38 | 38 |
end |
| 39 | 39 |
end |
| 40 |
+ |
|
| 41 |
+ def agent_dry_run_with_event_mode(agent) |
|
| 42 |
+ case |
|
| 43 |
+ when agent.cannot_receive_events? |
|
| 44 |
+ 'no'.freeze |
|
| 45 |
+ when agent.cannot_be_scheduled? |
|
| 46 |
+ # incoming event is the only trigger for the agent |
|
| 47 |
+ 'yes'.freeze |
|
| 48 |
+ else |
|
| 49 |
+ 'maybe'.freeze |
|
| 50 |
+ end |
|
| 51 |
+ end |
|
| 40 | 52 |
end |
@@ -7,7 +7,7 @@ |
||
| 7 | 7 |
|
| 8 | 8 |
<% if agent.can_dry_run? %> |
| 9 | 9 |
<li> |
| 10 |
- <%= link_to icon_tag('glyphicon-refresh') + ' Dry Run', '#', 'data-action-url' => dry_run_agent_path(agent), tabindex: "-1", onclick: "Utils.handleDryRunButton(this, '_method=PUT')" %>
|
|
| 10 |
+ <%= link_to icon_tag('glyphicon-refresh') + ' Dry Run', '#', 'data-action-url' => dry_run_agent_path(agent), 'data-with-event-mode' => agent_dry_run_with_event_mode(agent), tabindex: "-1", onclick: "Utils.handleDryRunButton(this)" %>
|
|
| 11 | 11 |
</li> |
| 12 | 12 |
<% end %> |
| 13 | 13 |
|
@@ -25,6 +25,6 @@ |
||
| 25 | 25 |
<div class="form-group"> |
| 26 | 26 |
<%= submit_tag "Save", :class => "btn btn-primary" %> |
| 27 | 27 |
<% if agent.can_dry_run? %> |
| 28 |
- <%= button_tag class: 'btn btn-default agent-dry-run-button', type: 'button', 'data-action-url' => agent.persisted? ? dry_run_agent_path(agent) : dry_run_agents_path do %><%= icon_tag('glyphicon-refresh') %> Dry Run<% end %>
|
|
| 28 |
+ <%= button_tag class: 'btn btn-default agent-dry-run-button', type: 'button', 'data-action-url' => agent.persisted? ? dry_run_agent_path(agent) : dry_run_agents_path, 'data-with-event-mode' => agent_dry_run_with_event_mode(agent) do %><%= icon_tag('glyphicon-refresh') %> Dry Run<% end %>
|
|
| 29 | 29 |
<% end %> |
| 30 | 30 |
</div> |
@@ -2,7 +2,7 @@ Huginn::Application.routes.draw do |
||
| 2 | 2 |
resources :agents do |
| 3 | 3 |
member do |
| 4 | 4 |
post :run |
| 5 |
- put :dry_run |
|
| 5 |
+ post :dry_run |
|
| 6 | 6 |
post :handle_details_post |
| 7 | 7 |
put :leave_scenario |
| 8 | 8 |
delete :remove_events |
@@ -7,10 +7,22 @@ describe DryRunnable do |
||
| 7 | 7 |
can_dry_run! |
| 8 | 8 |
|
| 9 | 9 |
def check |
| 10 |
+ perform |
|
| 11 |
+ end |
|
| 12 |
+ |
|
| 13 |
+ def receive(events) |
|
| 14 |
+ events.each do |event| |
|
| 15 |
+ perform(event.payload['prefix']) |
|
| 16 |
+ end |
|
| 17 |
+ end |
|
| 18 |
+ |
|
| 19 |
+ private |
|
| 20 |
+ |
|
| 21 |
+ def perform(prefix = nil) |
|
| 10 | 22 |
log "Logging" |
| 11 |
- create_event payload: { 'test' => 'foo' }
|
|
| 23 |
+ create_event payload: { 'test' => "#{prefix}foo" }
|
|
| 12 | 24 |
error "Recording error" |
| 13 |
- create_event payload: { 'test' => 'bar' }
|
|
| 25 |
+ create_event payload: { 'test' => "#{prefix}bar" }
|
|
| 14 | 26 |
self.memory = { 'last_status' => 'ok', 'dry_run' => dry_run? }
|
| 15 | 27 |
save! |
| 16 | 28 |
end |
@@ -46,21 +58,6 @@ describe DryRunnable do |
||
| 46 | 58 |
expect(messages).to eq(['Logging', 'Recording error']) |
| 47 | 59 |
end |
| 48 | 60 |
|
| 49 |
- it "traps logging, event emission and memory updating, with dry_run? returning true" do |
|
| 50 |
- results = nil |
|
| 51 |
- |
|
| 52 |
- expect {
|
|
| 53 |
- results = @agent.dry_run! |
|
| 54 |
- @agent.reload |
|
| 55 |
- }.not_to change {
|
|
| 56 |
- [@agent.memory, counts] |
|
| 57 |
- } |
|
| 58 |
- |
|
| 59 |
- expect(results[:log]).to match(/\AI, .+ INFO -- : Logging\nE, .+ ERROR -- : Recording error\n/) |
|
| 60 |
- expect(results[:events]).to eq([{ 'test' => 'foo' }, { 'test' => 'bar' }])
|
|
| 61 |
- expect(results[:memory]).to eq({ 'last_status' => 'ok', 'dry_run' => true })
|
|
| 62 |
- end |
|
| 63 |
- |
|
| 64 | 61 |
it "does not perform dry-run if Agent does not support dry-run" do |
| 65 | 62 |
stub(@agent).can_dry_run? { false }
|
| 66 | 63 |
|
@@ -77,4 +74,36 @@ describe DryRunnable do |
||
| 77 | 74 |
expect(results[:events]).to eq([]) |
| 78 | 75 |
expect(results[:memory]).to eq({})
|
| 79 | 76 |
end |
| 77 |
+ |
|
| 78 |
+ describe "dry_run!" do |
|
| 79 |
+ it "traps any destructive operations during a run" do |
|
| 80 |
+ results = nil |
|
| 81 |
+ |
|
| 82 |
+ expect {
|
|
| 83 |
+ results = @agent.dry_run! |
|
| 84 |
+ @agent.reload |
|
| 85 |
+ }.not_to change {
|
|
| 86 |
+ [@agent.memory, counts] |
|
| 87 |
+ } |
|
| 88 |
+ |
|
| 89 |
+ expect(results[:log]).to match(/\AI, .+ INFO -- : Logging\nE, .+ ERROR -- : Recording error\n/) |
|
| 90 |
+ expect(results[:events]).to eq([{ 'test' => 'foo' }, { 'test' => 'bar' }])
|
|
| 91 |
+ expect(results[:memory]).to eq({ 'last_status' => 'ok', 'dry_run' => true })
|
|
| 92 |
+ end |
|
| 93 |
+ |
|
| 94 |
+ it "traps any destructive operations during a run when an event is given" do |
|
| 95 |
+ results = nil |
|
| 96 |
+ |
|
| 97 |
+ expect {
|
|
| 98 |
+ results = @agent.dry_run!(Event.new(payload: { 'prefix' => 'super' }))
|
|
| 99 |
+ @agent.reload |
|
| 100 |
+ }.not_to change {
|
|
| 101 |
+ [@agent.memory, counts] |
|
| 102 |
+ } |
|
| 103 |
+ |
|
| 104 |
+ expect(results[:log]).to match(/\AI, .+ INFO -- : Logging\nE, .+ ERROR -- : Recording error\n/) |
|
| 105 |
+ expect(results[:events]).to eq([{ 'test' => 'superfoo' }, { 'test' => 'superbar' }])
|
|
| 106 |
+ expect(results[:memory]).to eq({ 'last_status' => 'ok', 'dry_run' => true })
|
|
| 107 |
+ end |
|
| 108 |
+ end |
|
| 80 | 109 |
end |
@@ -377,6 +377,19 @@ describe AgentsController do |
||
| 377 | 377 |
[users(:bob).agents.count, users(:bob).events.count, users(:bob).logs.count, agent.name, agent.updated_at] |
| 378 | 378 |
} |
| 379 | 379 |
end |
| 380 |
+ |
|
| 381 |
+ it "accepts an event" do |
|
| 382 |
+ sign_in users(:bob) |
|
| 383 |
+ agent = agents(:bob_website_agent) |
|
| 384 |
+ url_from_event = "http://xkcd.com/?from_event=1".freeze |
|
| 385 |
+ expect {
|
|
| 386 |
+ post :dry_run, id: agent, event: { url: url_from_event }
|
|
| 387 |
+ }.not_to change {
|
|
| 388 |
+ [users(:bob).agents.count, users(:bob).events.count, users(:bob).logs.count, agent.name, agent.updated_at] |
|
| 389 |
+ } |
|
| 390 |
+ json = JSON.parse(response.body) |
|
| 391 |
+ expect(json['log']).to match(/^I, .* : Fetching #{Regexp.quote(url_from_event)}$/)
|
|
| 392 |
+ end |
|
| 380 | 393 |
end |
| 381 | 394 |
|
| 382 | 395 |
describe "DELETE memory" do |